package com.ben.multiple.util;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * 加密/解密    工具
 *
 * @author ben
 * @date 2019/6/13
 */
public final class EncryptUtils {
    private EncryptUtils() {
    }

    /**
     * MD5 加密算法
     */
    private final static String MD5_ALGORITHM = "MD5";
    private final static char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    /**
     * aes  加密算法 默认： AES/ECB/PKCS5Padding
     */
    private final static String AES_ALGORITHM = "AES";
    private final static String AES_CBC_ALGORITHM = "AES/CBC/PKCS5Padding";

    /**
     * rsa 加密算法
     */
    private final static String RSA_ALGORITHM = "RSA";

    /**
     * aes 密钥字符长度
     */
    private final static int AES_SECRET_KEY_LENGTH = 16;
    /**
     * RAS 密钥生成器 初始化时，密钥的长度
     */
    private final static int RSA_KEY_SIZE = 1024;

    private final static Base64.Encoder ENCODER = Base64.getEncoder();
    private final static Base64.Decoder DECODER = Base64.getDecoder();

    private final static String ECC_ALGORITHM = "EC";

    /**
     * base64 加密
     *
     * @param content 需要加密的内容
     */
    public static String base64Encode(String content) {
        byte[] encode = ENCODER.encode(content.getBytes(StandardCharsets.UTF_8));
        return new String(encode, StandardCharsets.UTF_8);
    }

    /**
     * base64 解密
     *
     * @param content 需要解密的内容
     */
    public static String base64Decode(String content) {
        byte[] decode = DECODER.decode(content.getBytes(StandardCharsets.UTF_8));
        return new String(decode, StandardCharsets.UTF_8);
    }

    /**
     * MD5 加密   加密结果 长度 32 个字符
     *
     * @param content 需要加密的内容
     */
    public static String md5Encode(String content) throws NoSuchAlgorithmException {
        byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
        byte[] digest = MessageDigest.getInstance(MD5_ALGORITHM).digest(bytes);
        return new String(encodeHex(digest));
    }

//    AES

    /**
     * aes 加密   效率较快
     * "AES"   标准格式：算法/模式/填充   这里默认使用  AES/ECB/PKCS5Padding（输入可以不是16字节，也不需要填充向量）
     *
     * @param key     密钥
     * @param content 需要加密的内容
     */
    public static byte[] aesEncode(String key, String content) throws Exception {
        // 产生原始对称密钥
        String secretKey = md5Encode(key).substring(0, AES_SECRET_KEY_LENGTH);
        // 根据原始对称密钥 ，生成 AES 密钥
        SecretKeySpec aesSecretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        // 根据指定算法， 生成密码器
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        // 初始化密码器，第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作，第二个参数为  AES 密钥
        cipher.init(Cipher.ENCRYPT_MODE, aesSecretKey);
        // 加密
        return cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * aes 解密
     *
     * @param key     密钥
     * @param content 需要解密的内容
     */
    public static String aesDecode(String key, byte[] content) throws Exception {
        // 使用 md5 加密，获取 128位 初始密钥
        String secretKey = md5Encode(key).substring(0, AES_SECRET_KEY_LENGTH);
        // 根据原始对称密钥 生成 AES 密钥
        SecretKey aesSecretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        // 根据指定算法， 生成密码器
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        // 初始化密码器，第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作，第二个参数为  AES 密钥
        cipher.init(Cipher.DECRYPT_MODE, aesSecretKey);
        // 解密
        return new String(cipher.doFinal(content), StandardCharsets.UTF_8);
    }

    /**
     * aes 加密   使用  AES/CBC/PKCS5Padding        aes + md5
     *
     * @param key     密钥
     * @param content 需要加密的内容
     */
    public static byte[] aesMd5Encode(String key, String content) throws Exception {
        // 使用 md5 算法产生原始对称密钥 和 初始偏移参数
        String md5Key = md5Encode(key);
        String secretKey = md5Key.substring(0, AES_SECRET_KEY_LENGTH);
        String ivParam = md5Key.substring(AES_SECRET_KEY_LENGTH);
        // 根据原始对称密钥 生成 AES 密钥
        SecretKeySpec aesSecretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        // 根据指定算法  生成密码器
        Cipher cipher = Cipher.getInstance(AES_CBC_ALGORITHM);
        // 初始化密码器，第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作，第二个参数为  AES 密钥；使用 CBC 模式时，必须指定初始偏移参数
        cipher.init(Cipher.ENCRYPT_MODE, aesSecretKey, new IvParameterSpec(ivParam.getBytes(StandardCharsets.UTF_8)));
        // 加密
        return cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * aes 解密
     *
     * @param key     密钥
     * @param content 需要解密的内容
     */
    public static String aesMd5Decode(String key, byte[] content) throws Exception {
        // 使用 md5 算法产生原始对称密钥 和 初始偏移参数
        String md5Key = md5Encode(key);
        String secretKey = md5Key.substring(0, AES_SECRET_KEY_LENGTH);
        String ivParam = md5Key.substring(AES_SECRET_KEY_LENGTH);
        // 根据原始对称密钥 生成 AES 密钥
        SecretKeySpec aesSecretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        // 根据指定算法  生成密码器
        Cipher cipher = Cipher.getInstance(AES_CBC_ALGORITHM);
        // 初始化密码器，第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作，第二个参数为  AES 密钥
        cipher.init(Cipher.DECRYPT_MODE, aesSecretKey, new IvParameterSpec(ivParam.getBytes(StandardCharsets.UTF_8)));
        // 解密
        return new String(cipher.doFinal(content), StandardCharsets.UTF_8);
    }

    /**
     * aes 加密   标准格式：算法/模式/填充   这里使用  AES/CBC/PKCS5Padding      aes + md5 + base64
     *
     * @param key     密钥
     * @param content 需要加密的内容
     */
    public static String aesMd5Base64Encode(String key, String content) throws Exception {
        // 使用 md5 算法产生原始对称密钥 和 初始偏移参数
        String md5Key = md5Encode(key);
        String secretKey = md5Key.substring(0, AES_SECRET_KEY_LENGTH);
        String ivParam = md5Key.substring(AES_SECRET_KEY_LENGTH);
        // 根据原始对称密钥 生成 AES 密钥
        SecretKeySpec aesSecretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        // 根据指定算法 AES 生成密码器
        Cipher cipher = Cipher.getInstance(AES_CBC_ALGORITHM);
        // 初始化密码器，第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作，第二个参数为  AES 密钥；使用 CBC 模式时，必须指定初始偏移参数
        cipher.init(Cipher.ENCRYPT_MODE, aesSecretKey, new IvParameterSpec(ivParam.getBytes(StandardCharsets.UTF_8)));
        // 加密
        byte[] bytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
        // 将加密结果使用 base64 处理
        return new String(ENCODER.encode(bytes), StandardCharsets.UTF_8);
    }

    /**
     * aes 解密
     *
     * @param key     密钥
     * @param content 需要解密的内容
     */
    public static String aesMd5Base64Decode(String key, String content) throws Exception {
        // 使用 md5 算法产生原始对称密钥 和 初始偏移参数
        String md5Key = md5Encode(key);
        String secretKey = md5Key.substring(0, AES_SECRET_KEY_LENGTH);
        String ivParam = md5Key.substring(AES_SECRET_KEY_LENGTH);
        // 根据原始对称密钥 生成 AES 密钥
        SecretKeySpec aesSecretKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_ALGORITHM);
        // 根据指定算法 AES 生成密码器
        Cipher cipher = Cipher.getInstance(AES_CBC_ALGORITHM);
        // 初始化密码器，第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作，第二个参数为  AES 密钥；使用 CBC 模式时，必须指定初始偏移参数
        cipher.init(Cipher.DECRYPT_MODE, aesSecretKey, new IvParameterSpec(ivParam.getBytes(StandardCharsets.UTF_8)));
        // 使用 base64 将需要解密的内容进行预处理
        byte[] base64Content = DECODER.decode(content.getBytes(StandardCharsets.UTF_8));
        // aes 解密
        return new String(cipher.doFinal(base64Content), StandardCharsets.UTF_8);
    }

//    RSA

    /**
     * 生成公钥 、私钥 字符串
     */
    public static Map<String, String> generateRSAKeys() throws Exception {
        // 为RSA算法创建一个 KeyPairGenerator 对象
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
        // 初始化 KeyPairGenerator 对象，指定密钥长度
        keyPairGenerator.initialize(RSA_KEY_SIZE);
        //生成密匙对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        //得到公钥
        PublicKey publicKey = keyPair.getPublic();
        // base64 编码公钥 ，并转成字符串
        String publicKeyStr = new String(ENCODER.encode(publicKey.getEncoded()), StandardCharsets.UTF_8);
        //得到私钥
        Key privateKey = keyPair.getPrivate();
        // base64 编码私钥 ，并转成字符串
        String privateKeyStr = new String(ENCODER.encode(privateKey.getEncoded()), StandardCharsets.UTF_8);
        Map<String, String> keyPairMap = new HashMap<>(4);
        keyPairMap.put("publicKey", publicKeyStr);
        keyPairMap.put("privateKey", privateKeyStr);
        return keyPairMap;
    }

    /**
     * 公钥加密
     *
     * @param data      需要加密的数据
     * @param publicKey 公钥（经过 base64 加密过的字符串）
     */
    public static String rsaPublicKeyEncode(String data, String publicKey) throws Exception {
        // base64 解码 公钥
        byte[] decode = DECODER.decode(publicKey.getBytes(StandardCharsets.UTF_8));
        // 根据指定的算法，获取 keyFactory —— 转换 keyStr to Key 对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        // 通过 X509 编码的 Key 指令获得 公钥对象
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(decode));
        // 根据指定算法 RSA 生成密码器
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        // 密码器初始化，第一个参数是模式（加密/解密），第二个参数是公钥对象
        cipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey);
        //加密
        byte[] bytes = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(StandardCharsets.UTF_8), rsaPublicKey.getModulus().bitLength());
        // 加密结果使用 base64 编码，并转成字符串
        return new String(ENCODER.encode(bytes), StandardCharsets.UTF_8);
    }

    /**
     * 私钥解密
     *
     * @param data       需要解密的数据
     * @param privateKey 私钥（经过 base64 加密过的字符串）
     */
    public static String rsaPrivateKeyDecode(String data, String privateKey) throws Exception {
        // base64 解码
        byte[] decode = DECODER.decode(privateKey.getBytes(StandardCharsets.UTF_8));
        // 根据指定的算法，获取 keyFactory —— 转换 keyStr to Key 对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        //通过 PKCS8 编码的Key指令获得私钥对象
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decode));
        // 根据指定算法 RSA 生成密码器
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        // 密码器初始化，第一个参数是模式（加密/解密），第二个参数是私钥对象
        cipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey);
        //解密；先 base64 解码；再 rsa 解密
        byte[] dataBytes = DECODER.decode(data.getBytes(StandardCharsets.UTF_8));
        byte[] bytes = rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, dataBytes, rsaPrivateKey.getModulus().bitLength());
        // 将解密结果转换成字符串
        return new String(bytes, StandardCharsets.UTF_8);
    }

    /**
     * 私钥加密
     *
     * @param data       需要加密的数据
     * @param privateKey 私钥（经过 base64 加密过的字符串）
     */
    public static String rsaPrivateKeyEncode(String data, String privateKey) throws Exception {
        // base64 解码
        byte[] decode = DECODER.decode(privateKey.getBytes(StandardCharsets.UTF_8));
        // 根据指定的算法，获取 keyFactory —— 转换 keyStr to Key 对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        //通过 PKCS8 编码的Key指令获得私钥对象
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(decode));
        // 根据指定算法 RSA 生成密码器
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        // 密码器初始化，第一个参数是模式（加密/解密），第二个参数是私钥对象
        cipher.init(Cipher.ENCRYPT_MODE, rsaPrivateKey);
        // 加密
        byte[] bytes = rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(StandardCharsets.UTF_8), rsaPrivateKey.getModulus().bitLength());
        // 加密结果使用 base64 编码，并转成字符串
        return new String(ENCODER.encode(bytes), StandardCharsets.UTF_8);
    }

    /**
     * 公钥解密
     *
     * @param data      需要解密的数据
     * @param publicKey 公钥（经过 base64 加密过的字符串）
     */
    public static String rsaPublicKeyDecode(String data, String publicKey) throws Exception {
        // base64 解码 公钥
        byte[] decode = DECODER.decode(publicKey.getBytes(StandardCharsets.UTF_8));
        // 根据指定的算法，获取 keyFactory —— 转换 keyStr to Key 对象
        KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
        // 通过 X509 编码的 Key 指令获得 公钥对象
        RSAPublicKey rsaPublicKey = (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(decode));
        // 根据指定算法 RSA 生成密码器
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        // 密码器初始化，第一个参数是模式（加密/解密），第二个参数是公钥对象
        cipher.init(Cipher.DECRYPT_MODE, rsaPublicKey);
        //解密；先 base64 解码；再 rsa 解密
        byte[] dataBytes = DECODER.decode(data.getBytes(StandardCharsets.UTF_8));
        byte[] bytes = rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, dataBytes, rsaPublicKey.getModulus().bitLength());
        // 将解密结果转换成字符串
        return new String(bytes, StandardCharsets.UTF_8);
    }

    /**
     * 将一个字节数组转换为一个字符数组，按顺序表示每个字节的十六进制值。
     * 返回的数组的长度将是字节数组的两倍，两个字符来表示任何给定的字节。
     *
     * @param data byte[]
     * @return char[]
     */
    private static char[] encodeHex(final byte[] data) {
        if (data == null) {
            return null;
        }
        final int length = data.length;
        final char[] out = new char[length << 1];
        for (int i = 0, j = 0; i < length; i++) {
            out[j++] = DIGITS_LOWER[(0xF0 & data[i]) >>> 4];
            out[j++] = DIGITS_LOWER[0x0F & data[i]];
        }
        return out;
    }

    /**
     * 加密 / 解密      分组实现
     *
     * @param cipher  密码器
     * @param opmode  模式： 加密/解密
     * @param data    数据
     * @param keySize 密钥长度
     */
    private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] data, int keySize) throws Exception {
        // 单次 加密/解密 的最大数据大小
        int maxBlock;
        if (opmode == Cipher.DECRYPT_MODE) {
            //解密 ；1024——128
            maxBlock = keySize / 8;
        } else {
            //加密；1024——117
            maxBlock = keySize / 8 - 11;
        }
        //存储所有结果
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        //接收单次结果
        byte[] buff;
        int offSet = 0;
        int i = 0;
        //分组处理
        while (data.length > offSet) {
            if (data.length - offSet > maxBlock) {
                // 数据处理范围，数组偏移 offSet ；处理 offSet 至 offSet + maxBlock 范围的数据
                buff = cipher.doFinal(data, offSet, maxBlock);
            } else {
                buff = cipher.doFinal(data, offSet, data.length - offSet);
            }
            out.write(buff, 0, buff.length);
            i++;
            offSet = i * maxBlock;
        }
        byte[] result = out.toByteArray();
        out.close();
        return result;
    }
}
